@abraca/dabra 1.8.1 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abracadabra-provider.cjs +129 -3
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +129 -3
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +61 -1
- package/package.json +1 -1
- package/src/AbracadabraBaseProvider.ts +3 -3
- package/src/ContentManager.ts +160 -0
- package/src/DocConverters.ts +1707 -0
- package/src/DocTypes.ts +618 -0
- package/src/DocUtils.ts +89 -0
- package/src/DocumentManager.ts +342 -0
- package/src/E2EAbracadabraProvider.ts +189 -0
- package/src/FileBlobStore.ts +10 -0
- package/src/MetaManager.ts +100 -0
- package/src/TreeManager.ts +429 -0
- package/src/types.ts +9 -0
|
@@ -2468,12 +2468,13 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2468
2468
|
try {
|
|
2469
2469
|
const parsed = JSON.parse(payload);
|
|
2470
2470
|
if (parsed?.type === "error" && parsed.source && parsed.code) {
|
|
2471
|
-
const { source, code, message } = parsed;
|
|
2472
|
-
console.warn(`[Abracadabra] Server error: ${source} (${code}) — ${message}
|
|
2471
|
+
const { source, code, message, meta } = parsed;
|
|
2472
|
+
console.warn(`[Abracadabra] Server error: ${source} (${code}) — ${message}`, meta ?? "");
|
|
2473
2473
|
this.emit("serverError", {
|
|
2474
2474
|
source,
|
|
2475
2475
|
code,
|
|
2476
|
-
message: message ?? ""
|
|
2476
|
+
message: message ?? "",
|
|
2477
|
+
meta
|
|
2477
2478
|
});
|
|
2478
2479
|
return;
|
|
2479
2480
|
}
|
|
@@ -10455,6 +10456,15 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
10455
10456
|
this.objectUrls.delete(key);
|
|
10456
10457
|
}
|
|
10457
10458
|
}
|
|
10459
|
+
/**
|
|
10460
|
+
* Clear the 404 negative-cache entry for (docId, uploadId) so the next
|
|
10461
|
+
* getBlobUrl() re-fetches from the server instead of short-circuiting.
|
|
10462
|
+
* Use on explicit user retry or after reconnect, when the file may have
|
|
10463
|
+
* become available since the last 404.
|
|
10464
|
+
*/
|
|
10465
|
+
clearNotFound(docId, uploadId) {
|
|
10466
|
+
this._notFound.delete(this.blobKey(docId, uploadId));
|
|
10467
|
+
}
|
|
10458
10468
|
/** Revoke the object URL and remove the blob from cache. */
|
|
10459
10469
|
async evictBlob(docId, uploadId) {
|
|
10460
10470
|
const key = this.blobKey(docId, uploadId);
|
|
@@ -10855,6 +10865,19 @@ var E2EOfflineStore = class extends OfflineStore {
|
|
|
10855
10865
|
* are fetched on subsequent connects.
|
|
10856
10866
|
* - After sync, a fresh encrypted snapshot is saved.
|
|
10857
10867
|
*
|
|
10868
|
+
* Client-side compaction:
|
|
10869
|
+
* - After `compactionThreshold` encrypted updates have been applied in this
|
|
10870
|
+
* session (local + remote), and the doc has been quiescent for
|
|
10871
|
+
* `compactionQuiescenceMs`, the provider merges the whole Y.Doc, encrypts it,
|
|
10872
|
+
* and sends `snapshot:compact` — the server atomically replaces the per-doc
|
|
10873
|
+
* update log with that single compacted blob. The server acknowledges by
|
|
10874
|
+
* broadcasting `snapshot:compacted`, which emits the `"compacted"` event.
|
|
10875
|
+
* - Requires Owner or above (server silently drops non-Owner requests).
|
|
10876
|
+
* - Callers that want a final compaction before teardown should
|
|
10877
|
+
* `await provider.compactNow()` before `destroy()`. `destroy()` does not
|
|
10878
|
+
* compact (it'd race with the socket teardown) — any pending debounce is
|
|
10879
|
+
* cancelled.
|
|
10880
|
+
*
|
|
10858
10881
|
* Key availability limitation: if the user's WebAuthn key is not in
|
|
10859
10882
|
* DocKeyManager's in-memory cache and there is no network, E2E docs show
|
|
10860
10883
|
* empty — the key fetch requires either a cached in-memory key or network.
|
|
@@ -10862,6 +10885,11 @@ var E2EOfflineStore = class extends OfflineStore {
|
|
|
10862
10885
|
function fromBase64(b64) {
|
|
10863
10886
|
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
10864
10887
|
}
|
|
10888
|
+
function toBase64(bytes) {
|
|
10889
|
+
let bin = "";
|
|
10890
|
+
for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
10891
|
+
return btoa(bin);
|
|
10892
|
+
}
|
|
10865
10893
|
var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraProvider {
|
|
10866
10894
|
constructor(configuration) {
|
|
10867
10895
|
super({
|
|
@@ -10871,9 +10899,17 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10871
10899
|
this.docKey = null;
|
|
10872
10900
|
this.lastSeq = -1;
|
|
10873
10901
|
this.e2eStore = null;
|
|
10902
|
+
this.updatesSinceCompaction = 0;
|
|
10903
|
+
this.compactionInFlight = false;
|
|
10904
|
+
this.compactionInFlightTimeout = null;
|
|
10905
|
+
this.compactionDebounceTimer = null;
|
|
10906
|
+
this.destroyed = false;
|
|
10874
10907
|
this.docKeyManager = configuration.docKeyManager;
|
|
10875
10908
|
this.keystore = configuration.keystore;
|
|
10876
10909
|
this.e2eClient = configuration.client;
|
|
10910
|
+
this.compactionEnabled = configuration.compactionEnabled !== false;
|
|
10911
|
+
this.compactionThreshold = Math.max(1, configuration.compactionThreshold ?? 50);
|
|
10912
|
+
this.compactionQuiescenceMs = Math.max(0, configuration.compactionQuiescenceMs ?? 2e3);
|
|
10877
10913
|
this.e2eServerOrigin = E2EAbracadabraProvider.deriveServerOrigin(configuration, configuration.client);
|
|
10878
10914
|
}
|
|
10879
10915
|
/** Fetch the doc key from the server (requires WebAuthn if not cached). */
|
|
@@ -10885,6 +10921,10 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10885
10921
|
}
|
|
10886
10922
|
/** Handle stateless messages including e2e_ready and e2e_update. */
|
|
10887
10923
|
receiveStateless(payload) {
|
|
10924
|
+
if (payload.startsWith("snapshot:compacted ")) {
|
|
10925
|
+
this._handleCompactedBroadcast(payload.slice(19));
|
|
10926
|
+
return;
|
|
10927
|
+
}
|
|
10888
10928
|
let parsed;
|
|
10889
10929
|
try {
|
|
10890
10930
|
parsed = JSON.parse(payload);
|
|
@@ -10936,6 +10976,7 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10936
10976
|
const plaintext = await decryptField(encryptedData, key);
|
|
10937
10977
|
yjs.applyUpdate(this.document, plaintext, this);
|
|
10938
10978
|
this.lastSeq = Math.max(this.lastSeq, seq);
|
|
10979
|
+
this._noteUpdateApplied();
|
|
10939
10980
|
} catch (e) {
|
|
10940
10981
|
console.error("[E2EAbracadabraProvider] decryption failed for seq", seq, e);
|
|
10941
10982
|
}
|
|
@@ -10958,8 +10999,93 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10958
10999
|
update: encrypted,
|
|
10959
11000
|
documentName: this.configuration.name
|
|
10960
11001
|
});
|
|
11002
|
+
this._noteUpdateApplied();
|
|
11003
|
+
}
|
|
11004
|
+
/**
|
|
11005
|
+
* Force an immediate compaction attempt, bypassing the threshold and
|
|
11006
|
+
* quiescence debounce. Resolves once the `snapshot:compact` frame has been
|
|
11007
|
+
* sent (or rejected locally for missing prerequisites — destroyed, no
|
|
11008
|
+
* doc key, or already in-flight). The server acknowledges via a
|
|
11009
|
+
* `snapshot:compacted` broadcast, which emits the `"compacted"` event.
|
|
11010
|
+
*/
|
|
11011
|
+
async compactNow() {
|
|
11012
|
+
if (this.compactionDebounceTimer) {
|
|
11013
|
+
clearTimeout(this.compactionDebounceTimer);
|
|
11014
|
+
this.compactionDebounceTimer = null;
|
|
11015
|
+
}
|
|
11016
|
+
await this._performCompaction();
|
|
11017
|
+
}
|
|
11018
|
+
_noteUpdateApplied() {
|
|
11019
|
+
if (!this.compactionEnabled || this.destroyed) return;
|
|
11020
|
+
this.updatesSinceCompaction += 1;
|
|
11021
|
+
if (this.updatesSinceCompaction < this.compactionThreshold) return;
|
|
11022
|
+
if (this.compactionDebounceTimer) clearTimeout(this.compactionDebounceTimer);
|
|
11023
|
+
this.compactionDebounceTimer = setTimeout(() => {
|
|
11024
|
+
this.compactionDebounceTimer = null;
|
|
11025
|
+
this._performCompaction().catch((e) => {
|
|
11026
|
+
console.error("[E2EAbracadabraProvider] compaction failed:", e);
|
|
11027
|
+
});
|
|
11028
|
+
}, this.compactionQuiescenceMs);
|
|
11029
|
+
}
|
|
11030
|
+
async _performCompaction() {
|
|
11031
|
+
if (this.destroyed) return;
|
|
11032
|
+
if (this.compactionInFlight) return;
|
|
11033
|
+
if (this.updatesSinceCompaction === 0) return;
|
|
11034
|
+
if (!this.synced) return;
|
|
11035
|
+
const key = await this.ensureDocKey();
|
|
11036
|
+
if (!key) return;
|
|
11037
|
+
if (this.destroyed) return;
|
|
11038
|
+
this.compactionInFlight = true;
|
|
11039
|
+
try {
|
|
11040
|
+
const stateVector = yjs.encodeStateVector(this.document);
|
|
11041
|
+
const encrypted = await encryptField(yjs.encodeStateAsUpdate(this.document), key);
|
|
11042
|
+
if (this.destroyed) {
|
|
11043
|
+
this.compactionInFlight = false;
|
|
11044
|
+
return;
|
|
11045
|
+
}
|
|
11046
|
+
const payload = `snapshot:compact ${JSON.stringify({
|
|
11047
|
+
state_vector: toBase64(stateVector),
|
|
11048
|
+
compacted: toBase64(encrypted)
|
|
11049
|
+
})}`;
|
|
11050
|
+
this.sendStateless(payload);
|
|
11051
|
+
this.compactionInFlightTimeout = setTimeout(() => {
|
|
11052
|
+
this.compactionInFlight = false;
|
|
11053
|
+
this.compactionInFlightTimeout = null;
|
|
11054
|
+
}, 3e4);
|
|
11055
|
+
} catch (e) {
|
|
11056
|
+
this.compactionInFlight = false;
|
|
11057
|
+
throw e;
|
|
11058
|
+
}
|
|
11059
|
+
}
|
|
11060
|
+
_handleCompactedBroadcast(jsonStr) {
|
|
11061
|
+
let parsed = {};
|
|
11062
|
+
try {
|
|
11063
|
+
parsed = JSON.parse(jsonStr);
|
|
11064
|
+
} catch {}
|
|
11065
|
+
if (parsed.doc_id && parsed.doc_id !== this.configuration.name) return;
|
|
11066
|
+
if (this.compactionInFlightTimeout) {
|
|
11067
|
+
clearTimeout(this.compactionInFlightTimeout);
|
|
11068
|
+
this.compactionInFlightTimeout = null;
|
|
11069
|
+
}
|
|
11070
|
+
this.compactionInFlight = false;
|
|
11071
|
+
this.updatesSinceCompaction = 0;
|
|
11072
|
+
const event = {
|
|
11073
|
+
docId: parsed.doc_id ?? this.configuration.name,
|
|
11074
|
+
by: parsed.by
|
|
11075
|
+
};
|
|
11076
|
+
this.emit("compacted", event);
|
|
10961
11077
|
}
|
|
10962
11078
|
destroy() {
|
|
11079
|
+
if (this.destroyed) return;
|
|
11080
|
+
this.destroyed = true;
|
|
11081
|
+
if (this.compactionDebounceTimer) {
|
|
11082
|
+
clearTimeout(this.compactionDebounceTimer);
|
|
11083
|
+
this.compactionDebounceTimer = null;
|
|
11084
|
+
}
|
|
11085
|
+
if (this.compactionInFlightTimeout) {
|
|
11086
|
+
clearTimeout(this.compactionInFlightTimeout);
|
|
11087
|
+
this.compactionInFlightTimeout = null;
|
|
11088
|
+
}
|
|
10963
11089
|
this.e2eStore?.destroy();
|
|
10964
11090
|
this.e2eStore = null;
|
|
10965
11091
|
super.destroy();
|